package jmine.tec.executionlog;

import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.Principal;
import java.util.Calendar;
import java.util.LinkedList;

import javax.security.auth.Subject;

import jmine.tec.executionlog.dao.ExecutionLogDAO;
import jmine.tec.executionlog.dao.ExecutionLogItemDAO;
import jmine.tec.executionlog.domain.ExecutionLog;
import jmine.tec.executionlog.domain.ExecutionLogItem;
import jmine.tec.executionlog.domain.enumx.ExecutionItemStatus;
import jmine.tec.executionlog.domain.enumx.ExecutionStatus;
import jmine.tec.utils.UtilsMessages;
import bancosys.tec.persist.controller.TransactionalController;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.security.SecurityService;
import bancosys.tec.utils.date.Date;
import bancosys.tec.utils.date.Timestamp;
import bancosys.tec.utils.db.cleaner.CouldNotCleanException;

/**
 * Esse controller é responsável por guardar o contexto de execução de um driver. Todo cliente que chamar o método
 * <code>startExecution</code> deve garantir que o método <code>executionFinished</code> ou <code>executionFailed</code> foi chamado para
 * terminar essa thread de execução.
 * 
 * @author Cesar Olmos
 * @created Jun 19, 2008
 */
public class ExecutionLogController extends TransactionalController {
    private static final String SISTEMA = "Sistema";

    private static ThreadLocal<LinkedList<ExecutionLogContext>> executionStack = new ThreadLocal<LinkedList<ExecutionLogContext>>();

    /**
     * Testa se existe algum driver em execução.
     * 
     * @return <code>true</code> caso exista um driver em execução, <code>false</code> caso contrário.
     */
    public boolean isExecuting() {
        return this.getExecutionStack().size() > 0;
    }

    /**
     * Marca o início de execução de um driver.
     * 
     * @param fileCode o código do arquivo.
     * @param fileName nome do arquivo que esta sendo executado.
     * @param fileDate a data para os dados do arquivo.
     * @param origem interface que iniciou essa execução.
     * @param content o conteúdo do arquivo.
     * @return a execucao.
     */
    public ExecutionLog startExecution(String fileCode, String fileName, Date fileDate, String origem, InputStream content) {
        boolean success = false;
        this.startTransaction();
        try {
            ExecutionLogDAO dao = this.getDAOFactory().getDAOByClass(ExecutionLogDAO.class);
            ExecutionLog log = dao.createBean();

            log.setFileCode(fileCode);
            log.setFileName(fileName);
            log.setFileDate(fileDate);
            log.setContent(content);
            log.setExecutionStartTime(new Timestamp());
            log.setStatus(ExecutionStatus.RUNNING);
            log.setOrigem(origem == null ? SISTEMA : origem);
            log.setUserName(this.loadUserName());
            log.getPersister().save();
            log.initCachedInputStream();
            ExecutionLogContext context = new ExecutionLogContext();
            context.setIdExecutionLog(log.getId());
            this.getExecutionStack().add(context);
            success = true;
            return log;
        } finally {
            if (success) {
                this.commit();
            } else {
                this.rollback();
            }
        }
    }

    /**
     * Marca o início de execução de um driver. A data dos dados do arquivo será assumida como a data da importação.
     * 
     * @param fileCode o código do arquivo.
     * @param fileName nome do arquivo que esta sendo executado.
     * @param content o conteúdo do arquivo.
     * @return a execucao.
     */
    public ExecutionLog startExecution(String fileCode, String fileName, InputStream content) {
        return this.startExecution(fileCode, fileName, new Date(), null, content);
    }

    /**
     * Starts the execution of a ExecutionLog that already exists.
     * 
     * @param executionId executionId
     * @return ExecutionLog
     * @throws BeanNotFoundException BeanNotFoundException
     */
    public ExecutionLog startExistentExecution(Long executionId) throws BeanNotFoundException {
        boolean success = false;
        this.startTransaction();
        try {
            ExecutionLogDAO dao = this.getDAOFactory().getDAOByClass(ExecutionLogDAO.class);
            ExecutionLog log = dao.findByPk(executionId);
            log.initCachedInputStream();
            ExecutionLogContext context = new ExecutionLogContext();
            context.setIdExecutionLog(executionId);
            this.getExecutionStack().add(context);
            success = true;
            return log;
        } finally {
            if (success) {
                this.commit();
            } else {
                this.rollback();
            }
        }
    }

    /**
     * Adiciona um item executado com sucesso à execução corrente.
     * 
     * @param content o conteúdo desse item.
     * @param msg a mensagem que descreve esse item.
     */
    public void addSuccessfulItem(String content, String msg) {
        BaseDAO<ExecutionLogItem> dao = this.getDAOFactory().getGenericDAO(ExecutionLogItem.class);
        ExecutionLogItem logItem = dao.createBean();
        logItem.setContent(content);
        logItem.setStatus(ExecutionItemStatus.OK);
        logItem.setMessage(msg);
        this.addItem(logItem);
    }

    /**
     * Adiciona um item executado com erro à execução corrente.
     * 
     * @param content o conteúdo desse item.
     * @param msg a mensagem de erro.
     * @param throwable causa da falha da execução.
     */
    public void addErrorItem(String content, String msg, Throwable throwable) {
        BaseDAO<ExecutionLogItem> dao = this.getDAOFactory().getGenericDAO(ExecutionLogItem.class);
        ExecutionLogItem logItem = dao.createBean();
        logItem.setContent(content);
        logItem.setStatus(ExecutionItemStatus.NOT_OK);
        logItem.setMessage(msg);
        logItem.setStackTrace(this.getThrowableStackTrace(throwable));
        this.addItem(logItem);
    }

    /**
     * Adiciona um item executado com o status correspondente.
     * 
     * @param content o conteúdo dess item.
     * @param msg a mensagem que descreve o item.
     * @param status o status do item (Inserido, Alterado ou Ignorado)
     */
    public void addLogItem(String content, String msg, ExecutionItemStatus status) {
        BaseDAO<ExecutionLogItem> dao = this.getDAOFactory().getGenericDAO(ExecutionLogItem.class);
        ExecutionLogItem logItem = dao.createBean();
        logItem.setContent(content);
        logItem.setStatus(status);
        logItem.setMessage(msg);
        this.addItem(logItem);
    }

    /**
     * Adiciona um item não executado à execução corrente.
     * 
     * @param content o conteúdo desse item.
     * @param msg a mensagem de erro.
     */
    public void addNotExecutedItem(String content, String msg) {
        BaseDAO<ExecutionLogItem> dao = this.getDAOFactory().getGenericDAO(ExecutionLogItem.class);
        ExecutionLogItem logItem = dao.createBean();
        logItem.setContent(content);
        logItem.setStatus(ExecutionItemStatus.NOT_EXECUTED);
        logItem.setMessage(msg);
        this.addItem(logItem);
    }

    /**
     * Adds an item to the context, autosaving if needed.
     * 
     * @param logItem logItem
     */
    private void addItem(ExecutionLogItem logItem) {
        ExecutionLogContext context = this.getExecutionStack().getLast();
        context.addItem(logItem);
        if (context.shouldAutoSave()) {
            this.saveItems();
        }
    }

    /**
     * Habilita no contexto o modo em que cada {@link ExecutionLogItem} é salvo cada vez que é adicionado.
     */
    public void alwaysSaveMode() {
        ExecutionLogContext context = this.getExecutionStack().getLast();
        context.alwaysSaveMode();
    }

    /**
     * Saves unsaved items within a transaction.
     */
    private void saveItems() {
        boolean success = false;
        this.startTransaction();
        try {
            this.doSaveItems();
            success = true;
        } finally {
            if (success) {
                this.commit();
            } else {
                this.rollback();
            }
        }
    }

    /**
     * Saves unsaved items (expects a transaction to be available)
     */
    private void doSaveItems() {
        ExecutionLogContext context = this.getExecutionStack().getLast();
        ExecutionLog log = this.loadExecutionLog();
        for (ExecutionLogItem unsavedItem : context.retrieveUnsavedItems()) {
            unsavedItem.setExecutionLog(log);
            unsavedItem.getPersister().save();
        }
        context.clearUnsavedItems();
    }

    /**
     * Salva o log com status <code>OK</code> (caso nenhum addErrorItem tenha sido chamado) em uma transação separada.
     */
    public void executionFinished() {
        boolean success = false;
        this.startTransaction();
        try {
            this.doSaveItems();
            ExecutionLog execution = this.loadExecutionLog();
            execution.setStatus(this.getStatus(execution));
            execution.setExecutionEndTime(new Timestamp());
            execution.getPersister().save();
            this.getExecutionStack().removeLast();
            success = true;
        } finally {
            if (success) {
                this.commit();
            } else {
                this.rollback();
            }
        }
    }

    /**
     * Determina o status de um execution log a partir de seus itens.
     * 
     * @param executionLog o execution log.
     * @return o status de execução do execution log.
     */
    private ExecutionStatus getStatus(ExecutionLog executionLog) {
        ExecutionStatus status = ExecutionStatus.OK;
        ExecutionLogItemDAO itemDAO = this.daoFactory.getDAOByClass(ExecutionLogItemDAO.class);
        int totalItems = itemDAO.countByExecutionLog(executionLog);
        if (totalItems != 0) {
            if (executionLog.getTotalItems() == null || totalItems != executionLog.getTotalItems()) {
                return ExecutionStatus.NOT_OK;
            } else if (itemDAO.countByExecutionLogStatus(executionLog, ExecutionItemStatus.NOT_OK) != 0) {
                status = ExecutionStatus.NOT_OK;
            } else if (itemDAO.countByExecutionLogStatus(executionLog, ExecutionItemStatus.ALERT) != 0) {
                status = ExecutionStatus.ALERT;
            }
        }
        return status;
    }

    /**
     * Retorna o stack trace do throwable
     * 
     * @param throwable throwable
     * @return stack trace
     */
    private String getThrowableStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        throwable.printStackTrace(pw);
        return sw.toString();
    }

    /**
     * @return nome de usuario logado importando o arquivo ou "Sistema" caso nao haja usuario logado.
     */
    private String loadUserName() {
        String userName = SISTEMA;
        SecurityService securityService = this.getSecurityService();
        if (securityService == null) {
            throw new IllegalStateException("Servico de seguranca nao encontrado!");
        }

        Subject subject = securityService.getCurrentThreadSubject();
        if (subject != null) {
            Principal principal = subject.getPrincipals().iterator().next();
            userName = principal.getName();
        }
        return userName;
    }

    /**
     * @return execucao.
     */
    private ExecutionLog loadExecutionLog() {
        ExecutionLogDAO dao = this.getDAOFactory().getDAOByClass(ExecutionLogDAO.class);
        try {
            return dao.findByPk(this.getExecutionStack().getLast().getIdExecutionLog());
        } catch (BeanNotFoundException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Retorna true caso exista algum ExecutionLogItem no contexto com o status NOT_OK.
     * 
     * @return true caso exista algum ExecutionLogItem no contexto com o status NOT_OK.
     */
    public boolean hasUnsavedErrors() {
        ExecutionLogContext executionContext = this.getExecutionStack().getLast();
        for (ExecutionLogItem unsavedItem : executionContext.retrieveUnsavedItems()) {
            if (unsavedItem.getStatus().equals(ExecutionItemStatus.NOT_OK)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Adiciona aos itens atuais o número de itens passado
     * 
     * @param numItems o número de itens
     */
    public void addItemsInExecution(int numItems) {
        boolean success = false;
        this.startTransaction();
        try {
            ExecutionLog execution = this.loadExecutionLog();
            Integer currentItems = execution.getTotalItems();
            if (currentItems == null) {
                execution.setTotalItems(numItems);
            } else {
                execution.setTotalItems(currentItems + numItems);
            }
            execution.getPersister().save();
            success = true;
        } finally {
            if (success) {
                this.commit();
            } else {
                this.rollback();
            }
        }
    }

    /**
     * Define items totais de execucao.
     * 
     * @param totalItems quantidade total de itens a serem processados.
     */
    public void setTotalItemsInExecution(int totalItems) {
        boolean success = false;
        this.startTransaction();
        try {
            ExecutionLog execution = this.loadExecutionLog();
            execution.setTotalItems(totalItems);
            execution.getPersister().save();
            success = true;
        } finally {
            if (success) {
                this.commit();
            } else {
                this.rollback();
            }
        }
    }

    /**
     * Returns the thread's execution stack
     * 
     * @return LinkedList of ExecutionLogContext
     */
    private LinkedList<ExecutionLogContext> getExecutionStack() {
        LinkedList<ExecutionLogContext> stack = executionStack.get();
        if (stack == null) {
            stack = new LinkedList<ExecutionLogContext>();
            executionStack.set(stack);
        }
        return stack;
    }

    /**
     * Elimina do banco todos os registros anterior aos dias especificados no parâmetro. Apenas os registros ocorridos entre a data atual
     * menos os dias especificados e a data atual serão mantidos.
     * 
     * @param dias o numero de dias de que os registros serão mantidos.
     */
    public void cleanLog(Integer dias) {
        Timestamp tsdata = new Timestamp();
        tsdata.addDays(-1 * dias + 1);
        tsdata.setNanos(0);
        Calendar finalDate = tsdata.asCalendar();
        finalDate.set(Calendar.SECOND, 0);
        finalDate.set(Calendar.HOUR, 0);
        finalDate.set(Calendar.MINUTE, 0);
        Timestamp tsFinal = new Timestamp(finalDate);

        ExecutionLogDAO logDAO = this.getDAOFactory().getDAOByClass(ExecutionLogDAO.class);

        try {
            logDAO.removeByData(tsFinal);
        } catch (Exception e) {
            throw new CouldNotCleanException(UtilsMessages.ERROR_CLEANING_LOG_DB.create(), e);
        }
    }
}
